﻿using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web.Http;
using System.Web.Routing;
using Castle.Core.Internal;
using TaskRecord.Web.Common;
using RouteAttribute = System.Web.Http.RouteAttribute;

namespace TaskRecord.Web.TypedLink
{
    public static class RouteValuesExtractor
    {

        public static RouteModel ExtractRouteValues<TController, TResult>(Expression<Func<TController, TResult>> actionSelector)
        {
            RouteModel result = new RouteModel();

            Type controllerType = typeof(TController);

            var call = GetMethodCallExpression(actionSelector, controllerType);

            result.Controller = GetControllerName(controllerType);
            result.RouteName = GetRouteName<TController>(actionSelector.Body as MethodCallExpression);
            result.RouteValuesDictionary = GetRouteValues(call);

            return result;
        }

        private static string GetControllerName(Type controllerType)
        {
            var controllerName = controllerType.Name.EndsWith("Controller")
                ? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length)
                : controllerType.Name;
            return controllerName;
        }

        private static string GetRouteName<TController>(MethodCallExpression controllerMethodCall)
        {
            var methodRouteAttribute = controllerMethodCall.Method.GetCustomAttributes(typeof(RouteAttribute)).OfType<RouteAttribute>().ToList();

            if (methodRouteAttribute.Any())
            {
                var routeName = methodRouteAttribute.Single().Name;
                if (!routeName.IsNullOrEmpty())
                {
                    return routeName;
                }
            }

            if (!typeof(TController).IsSubclassOf(typeof(ApiController)))
            {
                throw new Exception("Api controller methods are required to have a RouteAttribute with the name property specified");
            }

            return "Default";
        }

        private static MethodCallExpression GetMethodCallExpression<TController, TResult>(Expression<Func<TController, TResult>> actionSelector, Type controllerType)
        {
            var call = actionSelector.Body as MethodCallExpression;
            if (call == null)
            {
                throw new ArgumentException("You must call a method of " + controllerType.Name, "actionSelector");
            }

            if (call.Object.Type != controllerType)
            {
                throw new ArgumentException("You must call a method of " + controllerType.Name, "actionSelector");
            }
            return call;
        }

        private static RouteValueDictionary GetRouteValues(MethodCallExpression call)
        {
            var routeValues = new RouteValueDictionary();

            var args = call.Arguments;
            ParameterInfo[] parameters = call.Method.GetParameters();
            var pairs = args.Select((a, i) => new
            {
                Argument = a,
                ParamName = parameters[i].Name
            });
            foreach (var argumentParameterPair in pairs)
            {
                string name = argumentParameterPair.ParamName;
                object value = argumentParameterPair.Argument.GetValue();
                if (value != null)
                {
                    var valueType = value.GetType();
                    if (valueType.IsValueType)
                    {
                        routeValues.Add(name, value);
                    }
                    else
                    {
                        var properties = PropertyInfoHelper.GetProperties(valueType);
                        foreach (var propertyInfo in properties)
                        {
                            routeValues.Add(propertyInfo.Name, propertyInfo.GetValue(value));
                        }
                    }
                }
            }
            return routeValues;
        }



        private static object GetValue(this Expression expression)
        {
            return GetExpressionValue(expression, null, null);
        }

        private static object GetExpressionValue(Expression expression, ReadOnlyCollection<ParameterExpression> parameters, object[] parameterValues)
        {
            switch (expression.NodeType)
            {
                case ExpressionType.Constant:
                    return ((ConstantExpression)expression).Value;
                case ExpressionType.MemberAccess:
                    {
                        var me = (MemberExpression)expression;
                        object obj = (me.Expression != null ? GetExpressionValue(me.Expression, parameters, parameterValues) : null);
                        if (me.Member is FieldInfo)
                            return ((FieldInfo)me.Member).GetValue(obj);
                        else if (me.Member is PropertyInfo)
                            return ((PropertyInfo)me.Member).GetValue(obj, null);
                        else
                            throw new NotSupportedException("Unsupported member access type");
                    }
                case ExpressionType.Parameter:
                    {
                        var pe = (ParameterExpression)expression;
                        for (int i = 0; i < parameters.Count; i++)
                        {
                            if (pe.Name == parameters[i].Name)
                                return parameterValues[i];
                        }
                        throw new InvalidOperationException("Invalid parameter");
                    }
                case ExpressionType.Convert:
                    {
                        var ue = (UnaryExpression)expression;
                        var operand = GetExpressionValue(ue.Operand, parameters, parameterValues);
                        if (ue.Type.IsInstanceOfType(operand))
                        {
                            return operand;
                        }
                        if (ue.Type.IsGenericType && ue.Type.GetGenericTypeDefinition() == typeof(Nullable<>) && ue.Type.GetGenericArguments()[0] == ue.Operand.Type)
                            return Activator.CreateInstance(typeof(Nullable<>).MakeGenericType(ue.Operand.Type), operand);
                        else
                            return Convert.ChangeType(operand, ue.Type);
                    }
                case ExpressionType.Call:
                    {
                        var ce = (MethodCallExpression)expression;
                        var target = (ce.Object != null ? GetExpressionValue(ce.Object, parameters, parameterValues) : null);
                        var args = ce.Arguments.Select(a => GetExpressionValue(a, parameters, parameterValues)).ToArray();
                        return ce.Method.Invoke(target, args);
                    }
                default:
                    var lambda = Expression.Lambda(expression, parameters).Compile();
                    return lambda.DynamicInvoke(parameterValues);
                //throw new InvalidOperationException("unsupported expression {0}".FormatWith(expression.ToString()));
            }
        }
    }
}
